#include <zenox/minilog.hpp>
#include <zenox/unicode.hpp>
#include <zenox/home.hpp>
#include <ctime>
#include <chrono>
#include <thread>
#include <fstream>
#include <iostream>

namespace zenox {

inline namespace minilog {

static constexpr char k_level_ansi_colors[(std::uint8_t)log_level::fatal + 1][8] = {
    "\x1b[37m",
    "\x1b[35m",
    "\x1b[32m",
    "\x1b[34m",
    "\x1b[33m",
    "\x1b[31m",
    "\x1b[31;1m",
};
static constexpr char k_reset_ansi_color[4] = "\x1b[m";

ZENO_CONST_FN static std::string log_level_name(log_level lev) {
    switch (lev) {
#define _FUNCTION(name) case log_level::name: return #name;
    ZENO_MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION
    }
    return "unknown";
}

ZENO_CONST_FN static log_level log_level_from_name(std::string lev) {
#define _FUNCTION(name) if (lev == #name) return log_level::name;
    ZENO_MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION
#ifndef NDEBUG
    return log_level::debug;
#else
    return log_level::info;
#endif
}

struct logger::Impl {
    log_level m_max_level = [] () -> log_level {
        if (auto lev = std::getenv("ZENO_MINILOG_LEVEL")) {
            return log_level_from_name(lev);
        }
#ifndef NDEBUG
        return log_level::debug;
#else
        return log_level::info;
#endif
    } ();

    std::ofstream m_log_file = [] () -> std::ofstream {
        if (auto path = std::getenv("ZENO_MINILOG_FILE")) {
            return std::ofstream(u8path(path));
        }
        return std::ofstream();
    } ();

    std::mutex m_mtx;
};

logger::logger() : m_impl(std::make_unique<Impl>()) {
}

logger::~logger() = default;

logger &logger::default_logger() {
    static logger instance;
    return instance;
}

static const std::thread::id g_main_thread_id = std::this_thread::get_id();

void logger::output_log(log_level lev, std::string_view msg, std::source_location const &loc) {
#if !defined(ZENO_HAS_CPP20_FORMAT) || defined(_WIN32)
    static char now[32];
    now[std::strftime(now, 31, "%F %T", std::localtime(&(time_t const &)std::time(nullptr)))] = 0;
#else
    std::chrono::zoned_time now{std::chrono::current_zone(), std::chrono::system_clock::now()};
#endif
    std::string_view filename = zeno_source_dir_prefix_remove(loc.file_name());
    auto m = ZENO_MINILOG_FMT_NAMESPACE::format("{} ({}:{}) [{}] {}", now, filename, loc.line(), log_level_name(lev), msg);
    if (auto tid = std::this_thread::get_id(); tid != g_main_thread_id) [[unlikely]] {
        std::ostringstream tidoss;
        tidoss << "(thread@" << std::hex << tid << ") ";
        m = tidoss.str() + m;
    }
    std::lock_guard _(m_impl->m_mtx);
    if (m_impl->m_log_file) [[likely]] {
        m_impl->m_log_file << m + '\n';
    }
    if (lev >= m_impl->m_max_level) [[unlikely]] {
        std::clog << (k_level_ansi_colors[(std::uint8_t)lev] + m) + k_reset_ansi_color + '\n' << std::flush;
    }
}

void logger::set_log_file(std::string path, bool override_environment_variable) {
    if (override_environment_variable || !m_impl->m_log_file) {
        m_impl->m_log_file = std::ofstream(u8path(path));
    } else {
        m_impl->m_log_file = std::ofstream(u8path(path));
    }
}

void logger::flush_log() {
    std::clog << std::flush;
    if (m_impl->m_log_file) [[likely]]
        m_impl->m_log_file << std::flush;
}

void logger::set_log_level(log_level lev) {
    m_impl->m_max_level = lev;
}

[[noreturn]] ZENO_NORETURN ZENO_COLD_FN void details::throw_fatal_exception(std::string_view msg, std::source_location const &loc) {
    logger::default_logger().output_log(log_level::fatal, msg, loc);
    std::string_view filename = zeno_source_dir_prefix_remove(loc.file_name());
    throw details::fatal_exception(ZENO_MINILOG_FMT_NAMESPACE::format("FATAL ERROR in {}:{}: [{}]", filename, loc.line(), msg));
}

details::fatal_exception::fatal_exception(std::string msg) noexcept
: m_msg(std::move(msg)) {}

const char *details::fatal_exception::what() const noexcept {
    return m_msg.c_str();
}

details::fatal_exception::~fatal_exception() noexcept = default;

}

}
